//  -------------------------------------------------------------------------
//  Copyright (C) 2015 BMW Car IT GmbH
//  -------------------------------------------------------------------------
//  This Source Code Form is subject to the terms of the Mozilla Public
//  License, v. 2.0. If a copy of the MPL was not distributed with this
//  file, You can obtain one at https://mozilla.org/MPL/2.0/.
//  -------------------------------------------------------------------------

#include "internal/RendererLib/SceneStateExecutor.h"
#include "internal/RendererLib/Renderer.h"
#include "internal/RendererLib/RendererScenes.h"
#include "internal/Components/ISceneGraphConsumerComponent.h"
#include "internal/RendererLib/RendererEventCollector.h"
#include "internal/Core/Utils/LogMacros.h"
#include "internal/PlatformAbstraction/Macros.h"
#include "internal/RendererLib/IRendererSceneEventSender.h"

namespace ramses::internal
{
    SceneStateExecutor::SceneStateExecutor(const Renderer& renderer, IRendererSceneEventSender& rendererSceneSender, RendererEventCollector& rendererEventCollector)
        : m_renderer(renderer)
        , m_rendererEventCollector(rendererEventCollector)
        , m_rendererSceneEventSender(rendererSceneSender)
    {
    }

    void SceneStateExecutor::setPublished(SceneId sceneId, EScenePublicationMode mode)
    {
        assert(checkIfCanBePublished(sceneId));
        m_scenesStateInfo.addScene(sceneId, mode);
        m_rendererEventCollector.addInternalSceneEvent(ERendererEventType::ScenePublished, sceneId);
        LOG_INFO(CONTEXT_RENDERER, "Scene {} is in state PUBLISHED", sceneId);
    }

    void SceneStateExecutor::setSubscriptionRequested(SceneId sceneId)
    {
        assert(checkIfCanBeSubscriptionRequested(sceneId));
        m_rendererSceneEventSender.sendSubscribeScene(sceneId);
        m_scenesStateInfo.setSceneState(sceneId, ESceneState::SubscriptionRequested);
        LOG_INFO(CONTEXT_RENDERER, "Scene {} is in state SUBSCRIPTION REQUESTED", sceneId);
    }

    void SceneStateExecutor::setSubscriptionPending(SceneId sceneId)
    {
        assert(checkIfCanBeSubscriptionPending(sceneId));
        m_scenesStateInfo.setSceneState(sceneId, ESceneState::SubscriptionPending);
        LOG_INFO(CONTEXT_RENDERER, "Scene {} is in state SUBSCRIPTION PENDING caused by SCENE RECEIVED", sceneId);
    }

    void SceneStateExecutor::setSubscribed(SceneId sceneId)
    {
        assert(checkIfCanBeSubscribed(sceneId));
        m_scenesStateInfo.setSceneState(sceneId, ESceneState::Subscribed);
        m_rendererEventCollector.addInternalSceneEvent(ERendererEventType::SceneSubscribed, sceneId);
        LOG_INFO(CONTEXT_RENDERER, "Scene {} is in state SUBSCRIBED caused by SCENE FLUSH", sceneId);
    }

    void SceneStateExecutor::rollBackToUnsubscribedAndTriggerIndirectEvents(ESceneState sceneState, SceneId sceneId)
    {
        switch (sceneState)
        {
        case ESceneState::Rendered:
            m_rendererEventCollector.addInternalSceneEvent(ERendererEventType::SceneHiddenIndirect, sceneId);
            RFALLTHROUGH;

        case ESceneState::RenderRequested:
            if (sceneState == ESceneState::RenderRequested)
            {
                m_rendererEventCollector.addInternalSceneEvent(ERendererEventType::SceneShowFailed, sceneId);
            }
            RFALLTHROUGH;

        case ESceneState::Mapped:
            m_rendererEventCollector.addInternalSceneEvent(ERendererEventType::SceneUnmappedIndirect, sceneId);
            RFALLTHROUGH;
        case ESceneState::MappingAndUploading:
        case ESceneState::MapRequested:
            if (sceneState == ESceneState::MapRequested || sceneState == ESceneState::MappingAndUploading)
            {
                m_rendererEventCollector.addInternalSceneEvent(ERendererEventType::SceneMapFailed, sceneId);
            }
            RFALLTHROUGH;
        case ESceneState::Subscribed:
            m_rendererEventCollector.addInternalSceneEvent(ERendererEventType::SceneUnsubscribedIndirect, sceneId);
            RFALLTHROUGH;
        case ESceneState::SubscriptionPending:
        case ESceneState::SubscriptionRequested:
            if (sceneState == ESceneState::SubscriptionPending || sceneState == ESceneState::SubscriptionRequested)
            {
                m_rendererEventCollector.addInternalSceneEvent(ERendererEventType::SceneSubscribeFailed, sceneId);
            }
            // explicitly unsubscribe to avoid race in case client re-published scene while previous subscription was being processed
            m_rendererSceneEventSender.sendUnsubscribeScene(sceneId);
            break;
        default:
            break;
        }
    }

    void SceneStateExecutor::setUnpublished(SceneId sceneId)
    {
        assert(checkIfCanBeUnpublished(sceneId));
        const ESceneState sceneState = m_scenesStateInfo.getSceneState(sceneId);
        rollBackToUnsubscribedAndTriggerIndirectEvents(sceneState, sceneId);
        if (m_scenesStateInfo.hasScene(sceneId))
        {
            m_scenesStateInfo.removeScene(sceneId);
            m_rendererEventCollector.addInternalSceneEvent(ERendererEventType::SceneUnpublished, sceneId);
        }
        else
        {
            LOG_ERROR(CONTEXT_RENDERER, "SceneStateExecutor::setSceneUnpublished Failed unpublish for scene with id :{} because state is inconsistent (state is {})!", sceneId, EnumToString(sceneState));
            assert(false);
        }

        LOG_INFO(CONTEXT_RENDERER, "Scene {} is in state UNPUBLISHED, scene was in state {}", sceneId, EnumToString(sceneState));
    }

    void SceneStateExecutor::setUnsubscribed(SceneId sceneId, bool indirect)
    {
        if (indirect)
        {
            const ESceneState sceneState = m_scenesStateInfo.getSceneState(sceneId);
            rollBackToUnsubscribedAndTriggerIndirectEvents(sceneState, sceneId);
        }
        else
        {
            assert(canBeUnsubscribed(sceneId));
            m_rendererEventCollector.addInternalSceneEvent(ERendererEventType::SceneUnsubscribed, sceneId);
            m_rendererSceneEventSender.sendUnsubscribeScene(sceneId);
        }

        m_scenesStateInfo.setSceneState(sceneId, ESceneState::Published);

        LOG_INFO(CONTEXT_RENDERER, "Scene {} is in state PUBLISHED caused by command UNSUBSCRIBE (indirect: {})", sceneId, indirect);
    }

    void SceneStateExecutor::setMapRequested(SceneId sceneId)
    {
        assert(canBeMapRequested(sceneId));
        m_scenesStateInfo.setSceneState(sceneId, ESceneState::MapRequested);
        LOG_INFO(CONTEXT_RENDERER, "Scene {} is in state MAP REQUESTED", sceneId);
    }

    void SceneStateExecutor::setMappingAndUploading(SceneId sceneId)
    {
        assert(canBeMappingAndUploading(sceneId));
        m_scenesStateInfo.setSceneState(sceneId, ESceneState::MappingAndUploading);
        LOG_INFO(CONTEXT_RENDERER, "Scene {} is in state MAPPING_AND_UPLOADING", sceneId);
    }

    void SceneStateExecutor::setMapped(SceneId sceneId)
    {
        assert(checkIfCanBeMapped(sceneId));
        m_scenesStateInfo.setSceneState(sceneId, ESceneState::Mapped);
        m_rendererEventCollector.addInternalSceneEvent(ERendererEventType::SceneMapped, sceneId);
        LOG_INFO(CONTEXT_RENDERER, "Scene {} is in state MAPPED caused by command MAP", sceneId);
    }

    void SceneStateExecutor::setUnmapped(SceneId sceneId)
    {
        assert(canBeUnmapped(sceneId));
        m_scenesStateInfo.setSceneState(sceneId, ESceneState::Subscribed);
        m_rendererEventCollector.addInternalSceneEvent(ERendererEventType::SceneUnmapped, sceneId);
        LOG_INFO(CONTEXT_RENDERER, "Scene {} is in state SUBSCRIBED caused by command UNMAP", sceneId);
    }

    void SceneStateExecutor::setRenderedRequested(SceneId sceneId)
    {
        assert(canBeRenderedRequested(sceneId));
        m_scenesStateInfo.setSceneState(sceneId, ESceneState::RenderRequested);
        LOG_INFO(CONTEXT_RENDERER, "Scene {} is in state RENDERED_REQUESTED", sceneId);
    }

    void SceneStateExecutor::setRendered(SceneId sceneId)
    {
        assert(canBeShown(sceneId));
        m_scenesStateInfo.setSceneState(sceneId, ESceneState::Rendered);
        m_rendererEventCollector.addInternalSceneEvent(ERendererEventType::SceneShown, sceneId);
        LOG_INFO(CONTEXT_RENDERER, "Scene {} is in state RENDERED caused by command SHOW", sceneId);
    }

    void SceneStateExecutor::setHidden(SceneId sceneId)
    {
        assert(canBeHidden(sceneId));
        m_scenesStateInfo.setSceneState(sceneId, ESceneState::Mapped);
        m_rendererEventCollector.addInternalSceneEvent(ERendererEventType::SceneHidden, sceneId);
        LOG_INFO(CONTEXT_RENDERER, "Scene {} is in state MAPPED caused by command HIDE", sceneId);
    }

    ESceneState SceneStateExecutor::getSceneState(SceneId sceneId) const
    {
        if (m_scenesStateInfo.hasScene(sceneId))
        {
            return m_scenesStateInfo.getSceneState(sceneId);
        }

        return ESceneState::Unknown;
    }

    std::optional<EScenePublicationMode> SceneStateExecutor::getScenePublicationMode(SceneId sceneId) const
    {
        if (m_scenesStateInfo.hasScene(sceneId))
        {
            return m_scenesStateInfo.getScenePublicationMode(sceneId);
        }

        return {};
    }

    bool SceneStateExecutor::checkIfCanBePublished(SceneId sceneId) const
    {
        if (!m_scenesStateInfo.hasScene(sceneId))
        {
            return true;
        }

        LOG_ERROR(CONTEXT_RENDERER, "Failed publication of scene with id :{} because scene with this id has already been published!", sceneId);
        return false;
    }

    bool SceneStateExecutor::checkIfCanBeUnpublished(SceneId sceneId) const
    {
        if (m_scenesStateInfo.hasScene(sceneId))
        {
            const ESceneState sceneState = m_scenesStateInfo.getSceneState(sceneId);
            if (ESceneState::Unknown != sceneState)
            {
                return true;
            }

            LOG_ERROR(CONTEXT_RENDERER, "Failed unpublish for scene with id :{} because state is inconsistent!", sceneId);
        }
        else
        {
            LOG_ERROR(CONTEXT_RENDERER, "Failed unpublish for scene with id :{} because it was not published!", sceneId);
        }

        return false;
    }

    bool SceneStateExecutor::checkIfCanBeSubscriptionRequested(SceneId sceneId) const
    {
        if (!canBeSubscriptionRequested(sceneId))
        {
            LOG_ERROR(CONTEXT_RENDERER, "Failed subscription request for scene with id :{} because it was not in published state!", sceneId);
            m_rendererEventCollector.addInternalSceneEvent(ERendererEventType::SceneSubscribeFailed, sceneId);
            return false;
        }

        return true;
    }

    bool SceneStateExecutor::checkIfCanBeUnsubscribed(SceneId sceneId) const
    {
        if (!canBeUnsubscribed(sceneId))
        {
            LOG_ERROR(CONTEXT_RENDERER, "Failed to unsubscribe from scene with id :{} because it is not in the subscribed/subscribing state!", sceneId);
            m_rendererEventCollector.addInternalSceneEvent(ERendererEventType::SceneUnsubscribeFailed, sceneId);
            return false;
        }

        return true;
    }

    bool SceneStateExecutor::checkIfCanBeMapRequested(SceneId sceneId) const
    {
        if (!canBeMapRequested(sceneId))
        {
            if (getSceneState(sceneId) == ESceneState::MapRequested || getSceneState(sceneId) == ESceneState::MappingAndUploading)
            {
                LOG_ERROR(CONTEXT_RENDERER, "Failed to map scene with id :{} because renderer is already mapping the scene", sceneId);
            }
            else
            {
                m_rendererEventCollector.addInternalSceneEvent(ERendererEventType::SceneMapFailed, sceneId);
                LOG_ERROR(CONTEXT_RENDERER, "Failed to map scene with id :{} because scene is not in subscribed state or display does not exist (state is {})!", sceneId, EnumToString(getSceneState(sceneId)));
            }
            return false;
        }

        return true;
    }

    bool SceneStateExecutor::checkIfCanBeMapped(SceneId sceneId) const
    {
        if (!canBeMapped(sceneId))
        {
            m_rendererEventCollector.addInternalSceneEvent(ERendererEventType::SceneMapFailed, sceneId);
            LOG_ERROR(CONTEXT_RENDERER, "Failed map for scene with id :{} because scene is not in mapping_and_uploading state (state is {})!", sceneId, EnumToString(getSceneState(sceneId)));
            return false;
        }

        return true;
    }

    bool SceneStateExecutor::checkIfCanBeUnmapped(SceneId sceneId) const
    {
        if (!canBeUnmapped(sceneId))
        {
            LOG_ERROR(CONTEXT_RENDERER, "Failed unmap for scene with id :{} because it is not in mapped or mapping state  (state is {})!", sceneId.getValue(), EnumToString(getSceneState(sceneId)));
            m_rendererEventCollector.addInternalSceneEvent(ERendererEventType::SceneUnmapFailed, sceneId);
            return false;
        }

        return true;
    }

    bool SceneStateExecutor::checkIfCanBeRenderedRequested(SceneId sceneId) const
    {
        if (!canBeRenderedRequested(sceneId))
        {
            m_rendererEventCollector.addInternalSceneEvent(ERendererEventType::SceneShowFailed, sceneId);
            LOG_ERROR(CONTEXT_RENDERER, "Failed show for scene with id :{} because scene is not in mapped state  (state is {})!", sceneId, EnumToString(getSceneState(sceneId)));
            return false;
        }

        return true;
    }

    bool SceneStateExecutor::checkIfCanBeShown(SceneId sceneId) const
    {
        if (!canBeShown(sceneId))
        {
            m_rendererEventCollector.addInternalSceneEvent(ERendererEventType::SceneShowFailed, sceneId);
            LOG_ERROR(CONTEXT_RENDERER, "Failed show for scene with id :{} because scene is not in mapped state  (state is {})!", sceneId, EnumToString(getSceneState(sceneId)));
            return false;
        }

        return true;
    }

    bool SceneStateExecutor::checkIfCanBeHidden(SceneId sceneId) const
    {
        if (!canBeHidden(sceneId))
        {
            LOG_ERROR(CONTEXT_RENDERER, "Failed hiding scene with id :{} because it is not in a shown state (state is {})!", sceneId, EnumToString(getSceneState(sceneId)));
            m_rendererEventCollector.addInternalSceneEvent(ERendererEventType::SceneHideFailed, sceneId);
            return false;
        }

        return true;
    }

    bool SceneStateExecutor::checkIfCanBeSubscriptionPending(SceneId sceneId) const
    {
        if (m_scenesStateInfo.hasScene(sceneId))
        {
            if (m_scenesStateInfo.getSceneState(sceneId) == ESceneState::SubscriptionRequested)
            {
                return true;
            }
            LOG_ERROR(CONTEXT_RENDERER, "Failed to receive scene with id {} because scene arrived before!", sceneId);
        }
        else
        {
            LOG_ERROR(CONTEXT_RENDERER, "Failed to receive scene with id {} because it is not known to the renderer!", sceneId);
        }

        return false;
    }

    bool SceneStateExecutor::checkIfCanBeSubscribed(SceneId sceneId) const
    {
        if (m_scenesStateInfo.hasScene(sceneId))
        {
            if (m_scenesStateInfo.getSceneState(sceneId) == ESceneState::SubscriptionPending)
            {
                return true;
            }
            LOG_ERROR(CONTEXT_RENDERER, "Failed to subscribe scene with id {} because scene is in wrong state {}!", sceneId, EnumToString(m_scenesStateInfo.getSceneState(sceneId)));
        }
        else
        {
            LOG_ERROR(CONTEXT_RENDERER, "Failed to subscribe scene with id {} because it is not known to the renderer!", sceneId);
        }

        return false;
    }

    bool SceneStateExecutor::canBeSubscriptionRequested(SceneId sceneId) const
    {
        const bool sceneIsPublished = (m_scenesStateInfo.hasScene(sceneId) && ESceneState::Published == m_scenesStateInfo.getSceneState(sceneId));
        if (sceneIsPublished)
        {
            return true;
        }

        LOG_WARN(CONTEXT_RENDERER, "Subscription request for scene with id :{} might fail because scene is in state {}", sceneId, EnumToString(m_scenesStateInfo.getSceneState(sceneId)));
        return false;
    }

    bool SceneStateExecutor::canBeUnsubscribed(SceneId sceneId) const
    {
        if (m_scenesStateInfo.hasScene(sceneId))
        {
            const ESceneState currentSceneState = m_scenesStateInfo.getSceneState(sceneId);
            switch (currentSceneState)
            {
            case ESceneState::SubscriptionRequested:
            case ESceneState::SubscriptionPending:
            case ESceneState::Subscribed:
                return true;
            default:
                LOG_WARN(CONTEXT_RENDERER, "Unsubscribe scene with id :{} might fail because scene is in state {}", sceneId, EnumToString(currentSceneState));
                break;
            }
        }
        else
        {
            LOG_WARN(CONTEXT_RENDERER, "Unsubscribe scene with id :{} might fail because scene was not published!", sceneId);
        }

        return false;
    }

    bool SceneStateExecutor::canBeMapRequested(SceneId sceneId) const
    {
        if (m_scenesStateInfo.hasScene(sceneId) && ESceneState::Subscribed == m_scenesStateInfo.getSceneState(sceneId))
        {
            if (m_renderer.hasDisplayController())
            {
                return true;
            }
            LOG_WARN(CONTEXT_RENDERER, "Map scene with id :{} might fail because the display seems invalid!", sceneId);
        }
        else
        {
            LOG_WARN(CONTEXT_RENDERER, "Map scene with id :{} might fail because scene is not in subscribed state!", sceneId);
        }

        return false;
    }

    bool SceneStateExecutor::canBeMappingAndUploading(SceneId sceneId) const
    {
        return (m_scenesStateInfo.hasScene(sceneId) && ESceneState::MapRequested == m_scenesStateInfo.getSceneState(sceneId));
    }

    bool SceneStateExecutor::canBeMapped(SceneId sceneId) const
    {
        return (m_scenesStateInfo.hasScene(sceneId) && ESceneState::MappingAndUploading == m_scenesStateInfo.getSceneState(sceneId));
    }

    bool SceneStateExecutor::canBeUnmapped(SceneId sceneId) const
    {
        const auto sceneState = m_scenesStateInfo.hasScene(sceneId) ? m_scenesStateInfo.getSceneState(sceneId) : ESceneState::Unknown;
        if (sceneState == ESceneState::Mapped ||
            sceneState == ESceneState::MapRequested ||
            sceneState == ESceneState::MappingAndUploading)
        {
            return true;
        }

        LOG_WARN(CONTEXT_RENDERER, "Unmap scene with id :{} might fail because scene is not in mapped or mapping state  (state is {})", sceneId, EnumToString(getSceneState(sceneId)));
        return false;
    }

    bool SceneStateExecutor::canBeRenderedRequested(SceneId sceneId) const
    {
        if (m_scenesStateInfo.hasScene(sceneId) && ESceneState::Mapped == m_scenesStateInfo.getSceneState(sceneId))
        {
            return true;
        }

        LOG_WARN(CONTEXT_RENDERER, "Show scene with id :{} might fail because scene is not in mapped state (state is {})", sceneId, EnumToString(getSceneState(sceneId)));
        return false;
    }

    bool SceneStateExecutor::canBeShown(SceneId sceneId) const
    {
        if (m_scenesStateInfo.hasScene(sceneId) && ESceneState::RenderRequested == m_scenesStateInfo.getSceneState(sceneId))
        {
            return true;
        }

        LOG_WARN(CONTEXT_RENDERER, "Show scene with id :{} might fail because scene is not in rendered requested state(state is{})", sceneId, EnumToString(getSceneState(sceneId)));
        return false;
    }

    bool SceneStateExecutor::canBeHidden(SceneId sceneId) const
    {
        if (m_scenesStateInfo.hasScene(sceneId)
            && (m_scenesStateInfo.getSceneState(sceneId) == ESceneState::Rendered || m_scenesStateInfo.getSceneState(sceneId) == ESceneState::RenderRequested))
        {
            return true;
        }

        LOG_WARN(CONTEXT_RENDERER, "Hide scene with id :{} might fail because scene is not in rendered state (state is {})!", sceneId, EnumToString(getSceneState(sceneId)));
        return false;
    }

}
